
import * as utc from "../util/utc.js";
import * as _ from "underscore";
import {loadJsonOnce} from "../network/fetcher.js";
import {newProduct} from "./productUtils.js";
import {tr} from "../ux/translations.js";
import * as sources from "./sources.js";
import {merge} from "../util/arrays.js";
import {regularGrid} from "../grid/regular.js";
import * as nearest from "../interpolate/nearest.js";
import * as bilinear from "../interpolate/bilinear.js";
import {mulvec2, length} from "../util/math.js";
import {ø} from "../util/objects.js";
import {createUnitDescriptors} from "../util/units.js";
import {gaiaPath} from "./gaia.js";
import {createCurrentsPalette} from "../palette/currents.js";


let palette;

/**
 * Returns the file name for the most recent OSCAR data layer to the specified date. If offset is non-zero,
 * the file name that many entries from the most recent is returned.
 *
 * The result is undefined if there is no entry for the specified date and offset can be found.
 *
 * UNDONE: the catalog object itself should encapsulate this logic. GFS can also be a "virtual" catalog, and
 *         provide a mechanism for eliminating the need for /data/weather/current/* files.
 *
 * @param {Array} catalog array of file names, sorted and prefixed with YYYYMMDD. Last item is most recent.
 * @param {Object} time_cursor parts or "current"
 * @param {Number?} offset
 * @returns {String} file name
 */
function lookupOscar(catalog, time_cursor, offset) {
    offset = +offset || 0;
    if (time_cursor === "current") {
        return catalog[catalog.length - 1 + offset];
    }
    const prefix = utc.print(time_cursor, "{YYYY}{MM}{DD}");
    let i = _.sortedIndex(catalog, prefix);
    i = (catalog[i] || "").indexOf(prefix) === 0 ? i : i - 1;
    return catalog[i + offset];
}

/**
 * @param catalog
 * @param time_cursor the date parts
 * @returns {Object} date parts
 */
function oscarValidTime(catalog, time_cursor) {
    const file = lookupOscar(catalog, time_cursor);
    return file ? utc.parse(file, /(\d{4})(\d\d)(\d\d)/) : null;
}

/**
 * @param {Array} catalog array of file names, sorted and prefixed with YYYYMMDD. Last item is most recent.
 * @param {Object} validTime date parts
 * @param {number} step
 * @returns {Object} the chronologically next or previous OSCAR data layer. How far forward or backward in
 * time to jump is determined by the step and the catalog of available layers. A step of ±1 moves to the
 * next/previous entry in the catalog (about 5 days), and a step of ±10 moves to the entry six positions away
 * (about 30 days).
 */
function oscarStep(catalog, validTime, step) {
    const file = lookupOscar(catalog, validTime, step);
    return file ? utc.parse(file, /(\d{4})(\d\d)(\d\d)/) : undefined;
}

function oscarPath(catalog, attr) {
    const file = lookupOscar(catalog, attr.time_cursor);
    return file ? gaiaPath("data/oscar", file) : null;
}

function fetchOscarCatalog() {
    // The OSCAR catalog is an array of file names, sorted and prefixed with YYYYMMDD. Last item is the
    // most recent. For example: [ 20140101-abc.epak, 20140106-abc.epak, 20140112-abc.epak, ... ]
    return loadJsonOnce(gaiaPath("data/oscar", "oscar-catalog.json"));
}

export function buildOscar(file) {
    const epak = file, header = epak.header, vars = header.variables;
    const u = vars["u"];
    const v = vars["v"];

    // dims are: time,depth,lat,lon
    const time = vars[u.dimensions[0]];
    const lat = vars[u.dimensions[2]];
    const lon = vars[u.dimensions[3]];
    const data = merge(epak.blocks[u.data.block], epak.blocks[v.data.block]);
    data.containsNaN = true;

    const grid = regularGrid(lon.sequence, lat.sequence);
    const field = {
        valueAt: i => {
            const j = i * 2;
            const u = data[j  ];
            const v = data[j+1];
            return [u, v];
        },
        scalarize: length,
        isDefined: i => !isNaN(data[i * 2]),
        nearest: nearest.vector(grid, data),
        bilinear: bilinear.vector(grid, data),
    };

    return {
        validTime: () => utc.parts(time.data[0]),
        grid: () => grid,
        field: () => field,
        valueInRange(t) { return [this.scale.valueInRange(t), 0]; },
    };
}

function doCreateCurrentsLayer(catalog, attr) {
    const validTime = oscarValidTime(catalog, attr.time_cursor);
    const stepper = step => function() {
        return oscarStep(catalog, this.validTime(), step);
    };

    return ø(newProduct(), {
        type: "currents",
        descriptionHTML: {
            name: tr`Ocean Currents`,
            qualifier: ` @ ${tr`Surface`}`,
        },
        sourceHTML: sources.oscar,
        paths: [oscarPath(catalog, attr)],
        validTime: () => ø(validTime),
        prev2: stepper(-6),
        prev1: stepper(-1),
        next1: stepper(+1),
        next2: stepper(+6),
        navLabels: {
            prev2: `-1 ${tr`month`}`,
            prev1: `-5 ${tr`days`}`,
            next1: `+5 ${tr`days`}`,
            next2: `+1 ${tr`month`}`,
        },
        builder: buildOscar,
        unitDescriptors: createUnitDescriptors({
            "m/s":  {                                      scalarize: length, precision: 2, convention: "with"},
            "km/h": {convert: uv => mulvec2(uv, 3.6),      scalarize: length, precision: 1, convention: "with"},
            "kn":   {convert: uv => mulvec2(uv, 1.943844), scalarize: length, precision: 1, convention: "with"},
            "mph":  {convert: uv => mulvec2(uv, 2.236936), scalarize: length, precision: 1, convention: "with"},
        }),
        scale: palette ??= createCurrentsPalette(),
        particles: {velocityScale: 1/7, maxIntensity: 0.7},
    });
}

export function createCurrentsLayer(attr) {
    return fetchOscarCatalog().then(catalog => doCreateCurrentsLayer(catalog, attr));
}
